﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data;
using System.IO;
using System.Xml.Linq;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using NLog;
using NLog.Targets;
using NLog.Config;
using System.Text;
using System.Security.Cryptography;
using System.Data.SqlClient;
using System.Globalization;
using System.Configuration;

namespace Tempestive.OAuthServer
{
    public class Manager
    {
        private static int CODE_EXPIRATION = 60; // seconds
        private static int TOKEN_EXPIRATION = 3600; // seconds
        private static string CODE_PREPEND = "code_";
        private static string TOKEN_PREPEND = "token_";
        private static string REFRESH_PREPEND = "refresh_";
        static Logger logger = LogManager.GetLogger("OAUTH_LOG");

        //PUBLIC

        public static void Authorize(HttpRequest request, HttpResponse response)
        {
            Cleaning();
            logger.Debug("Start Authorize");
            logger.Debug(request.UserHostAddress.ToString());

            string client_id = request["client_id"];
            string response_type = request["response_type"];
            string redirect_uri = request["redirect_uri"];
            string state = request["state"];
            string scope = request["scope"];
            logger.Debug("client_id:" + client_id);
            logger.Debug("response_type:" + response_type);
            logger.Debug("redirect_uri:" + redirect_uri);
            logger.Debug("state:" + state);
            logger.Debug("scope:" + scope);
            if (response_type != "code")
            {
                Error(response, redirect_uri, "unsupported_response_type", "Only code response_type is supported", state);
                logger.Warn("unsupported_response_type:" + state);
                return;
            }
            if (!VerifyClientId(response, client_id, redirect_uri, state))
            {
                logger.Warn("Validation Error");
                return;
            }
            HttpContext.Current.Session.Add("client_id", client_id);
            HttpContext.Current.Session.Add("response_type", response_type);
            HttpContext.Current.Session.Add("redirect_uri", redirect_uri);
            HttpContext.Current.Session.Add("state", state);
            HttpContext.Current.Session.Add("scope", scope);
            response.Redirect("./Default.aspx");
        }

        public static void Authenticate(HttpRequest request, HttpResponse response)
        {
            logger.Debug("Start Authentication: ");
            string user = request["user"];
            string password = request["password"];
            logger.Debug("user:" + user);

            if (HttpContext.Current.Session["client_id"] == null)
            {
                logger.Warn("Session Expired");
                response.Redirect("./expired.html");
                return;
            }

            string client_id = HttpContext.Current.Session["client_id"].ToString();
            List<ClientData> data = GetClientData();
            string auth_group = data.Where(r => r.client_id == client_id).ElementAt(0).auth_group;

            User user1 = VerifyAuthentication(response, user, password, auth_group);
            if (!user1.valid)
            {
                logger.Warn("Authentication failed");
                response.Redirect("./Default.aspx?error=error");
                return;
            }
            logger.Debug("Authentication successed");
            System.Web.SessionState.HttpSessionState session = HttpContext.Current.Session;
            string code = GetRandomCode(new Random());
            HttpContext.Current.Application.Add(CODE_PREPEND + code, user1);

            string urlRedirect = "{0}?code={1}";
            urlRedirect = string.Format(urlRedirect, session["redirect_uri"], code);
            string state = session["state"] as string;
            if (state != null && state != "")
            {
                urlRedirect += "&state=" + state;
            }
            logger.Debug("Redirect to:" + urlRedirect);
            response.Redirect(urlRedirect);
        }

        public static void GetToken(HttpRequest request, HttpResponse response)
        {
            logger.Debug("Start GetToken");
            Cleaning();
            System.Web.SessionState.HttpSessionState session = HttpContext.Current.Session;
            string grant_type = request["grant_type"];
            string client_id = request["client_id"];
            string client_secret = request["client_secret"];
            string code = request["code"];
            string redirect_uri = request["redirect_uri"];
            logger.Debug("grant_type:" + grant_type);
            logger.Debug("client_id:" + client_id);
            logger.Debug("client_secret:" + client_secret);
            logger.Debug("code:" + code);
            logger.Debug("redirect_uri:" + redirect_uri);
            if (grant_type == "authorization_code")
            {
                if (!VerifyClientIdAndSecret(response, client_id, client_secret, redirect_uri))
                {
                    return;
                }
                if (HttpContext.Current.Application[CODE_PREPEND + code] == null)
                {
                    Error(response, redirect_uri, "invalid_grant", "Not valid code.", null);
                    logger.Warn("Not valid code.");
                    return;
                }
                User userData = (User)HttpContext.Current.Application[CODE_PREPEND + code];
                if (DateTime.Now.Subtract(userData.timestamp).TotalSeconds > CODE_EXPIRATION)
                {
                    Error(response, redirect_uri, "invalid_grant", "Code is expired. Max duration is " + CODE_EXPIRATION + " sec.", null);
                    logger.Warn("Code is expired. Max duration is " + CODE_EXPIRATION + " sec.");
                }
                Random rdm = new Random();
                Token t = new Token(GetRandomCode(rdm), GetRandomCode(rdm), TOKEN_EXPIRATION);
                HttpContext.Current.Application.Add(TOKEN_PREPEND + t.access_token, userData);
                HttpContext.Current.Application.Add(REFRESH_PREPEND + t.refresh_token, t);
                response.ContentType = "application/json";
                response.AddHeader("Cache-Control", "no-store");
                logger.Debug("Return Token:" + Newtonsoft.Json.Linq.JObject.FromObject(t).ToString());
                response.Write(Newtonsoft.Json.Linq.JObject.FromObject(t).ToString());
            }
            else if (grant_type == "refresh_token")
            {
                if (!VerifyClientIdAndSecret(response, client_id, client_secret, redirect_uri))
                {
                    logger.Warn("Validation Error");
                    return;
                }
                string refresh = request["refresh_token"];
                Token t = (Token)HttpContext.Current.Application[REFRESH_PREPEND + refresh];
                User userData = (User)HttpContext.Current.Application[TOKEN_PREPEND + t.access_token];
                // delete old
                HttpContext.Current.Application.Remove(TOKEN_PREPEND + t.access_token);
                HttpContext.Current.Application.Remove(REFRESH_PREPEND + t.refresh_token);
                //save new
                Random rdm = new Random();
                t = new Token(GetRandomCode(rdm), GetRandomCode(rdm), TOKEN_EXPIRATION);
                HttpContext.Current.Application.Add(TOKEN_PREPEND + t.access_token, userData);
                HttpContext.Current.Application.Add(REFRESH_PREPEND + t.refresh_token, t);

                response.ContentType = "application/json";
                response.CacheControl = "no-store";
                logger.Debug("Return Token:" + Newtonsoft.Json.Linq.JObject.FromObject(t).ToString());
                response.Write(Newtonsoft.Json.Linq.JObject.FromObject(t).ToString());
            }
            else
            {
                Error(response, session["redirect_uri"] as string, "invalid_grant", "The provided access grant is invalid", session["state"] as string);
                logger.Warn("The provided access grant is invalid");
            }
        }

        public static void UserInfo(HttpRequest request, HttpResponse response)
        {
            logger.Debug("Start UserInfo");
            Cleaning();
            System.Web.SessionState.HttpSessionState session = HttpContext.Current.Session;
            string oauth_token = request["oauth_token"];
            logger.Debug("oauth_token:" + oauth_token);
            if (HttpContext.Current.Application[TOKEN_PREPEND + oauth_token] == null) //************* or expired
            {
                Error(response, session["redirect_uri"] as string, "invalid_token", "Invalid", session["state"] as string);
                logger.Warn("Invalid token:" + oauth_token);
                return;
            }
            User userData = (User)HttpContext.Current.Application[TOKEN_PREPEND + oauth_token];
            if ((DateTime.Now - userData.timestamp).TotalSeconds > TOKEN_EXPIRATION)
            {
                Error(response, session["redirect_uri"] as string, "expired_token", "Token is expired.", session["state"] as string);
                logger.Warn("Token is expired.");
                return;
            }
            logger.Debug("Return User:" + (Newtonsoft.Json.Linq.JObject.FromObject(userData).ToString()));
            response.Write(Newtonsoft.Json.Linq.JObject.FromObject(userData).ToString());
        }

        //PRIVATE

        private static void Error(HttpResponse response, string redirect_uri, string error, string error_description, string state)
        {
            string urlRedirect = "{0}?error={1}&error_description={2}";
            if (state != null && state != "")
            {
                urlRedirect += "&state=" + state;
            }
            urlRedirect = string.Format(urlRedirect, redirect_uri, error, error_description, state);
            response.Redirect(urlRedirect);
        }

        private static bool VerifyClientId(HttpResponse response, string client_id, string redirect_uri, string state)
        {
            List<ClientData> data = GetClientData();
            if (data.Where(r => r.client_id == client_id).Count() == 0)
            {
                Error(response, redirect_uri, "invalid_client", "The client identifier provided is invalid.", state);
                logger.Warn("The client identifier provided is invalid.");
                return false;
            }
            if (!redirect_uri.StartsWith(data.Where(r => r.client_id == client_id).First().redirect_uri))
            {
                Error(response, redirect_uri, "redirect_uri_mismatch", "The redirection URI provided does not match a pre-registered value.", state);
                logger.Warn("The redirection URI provided does not match a pre-registered value.");
                return false;
            }
            return true;
        }

        private static bool VerifyClientIdAndSecret(HttpResponse response, string client_id, string client_secret, string redirect_uri)
        {
            List<ClientData> data = GetClientData();
            if (data.Where(r => r.client_id == client_id).Count() == 0)
            {
                Error(response, redirect_uri, "invalid_client", "The client identifier provided is invalid.", null);
                logger.Warn("The client identifier provided is invalid.");
                return false;
            }
            if (!redirect_uri.StartsWith(data.Where(r => r.client_id == client_id).First().redirect_uri))
            {
                Error(response, redirect_uri, "redirect_uri_mismatch", "The redirection URI provided does not match a pre-registered value.", null);
                logger.Warn("The redirection URI provided does not match a pre-registered value.");
                return false;
            }
            if (data.Where(r => r.client_id == client_id).First().client_secret != client_secret)
            {
                Error(response, redirect_uri, "unauthorized_client", "Bad client_secret. Client secret sent is: " +
                    data.Where(r => r.client_id == client_id).First().client_secret, null);
                logger.Warn("Bad client_secret. Client secret sent is: " + data.Where(r => r.client_id == client_id).First().client_secret);
                return false;
            }
            return true;
        }

        private static List<ClientData> GetClientData()
        {
            List<ClientData> data = new List<ClientData>();
            int count = 1;
            while (ConfigurationManager.AppSettings["APPDATA_" + count + "_client_id"] != null)
            {
                string client_id = ConfigurationManager.AppSettings["APPDATA_" + count + "_client_id"];
                string client_secret = ConfigurationManager.AppSettings["APPDATA_" + count + "_client_secret"];
                string redirect_uri = ConfigurationManager.AppSettings["APPDATA_" + count + "_redirect_uri"];
                string auth_group = ConfigurationManager.AppSettings["APPDATA_" + count + "_auth_group"];
                data.Add(new ClientData(client_id, client_secret, redirect_uri, auth_group));
                count++;
            }
            return data;
        }

        private static User VerifyAuthentication(HttpResponse response, string user, string password, string authGroup)
        {
            string ldapserver = System.Configuration.ConfigurationManager.AppSettings["LDAPSERVER"];
            string ldapuser = System.Configuration.ConfigurationManager.AppSettings["LDAPUSER"];
            string ldappassword = System.Configuration.ConfigurationManager.AppSettings["LDAPPASSWORD"];

            PrincipalContext adContext = new PrincipalContext(ContextType.Domain, ldapserver);
            using (adContext)
            {
                if (adContext.ValidateCredentials(user, password))
                {
                    using (System.DirectoryServices.DirectorySearcher search =
                        new System.DirectoryServices.DirectorySearcher(
                            new System.DirectoryServices.DirectoryEntry("LDAP://" + ldapserver, ldapuser, ldappassword)))
                    {
                        if (authGroup != null && authGroup != "")
                        {
                            search.Filter = String.Format("(&(SAMAccountName={0})(memberOf={1}))", user, authGroup);
                        }
                        else
                        {
                            search.Filter = String.Format("(SAMAccountName={0})", user);
                        }
                        search.PropertiesToLoad.Add("displayName");
                        search.PropertiesToLoad.Add("givenName");
                        search.PropertiesToLoad.Add("sn");
                        search.PropertiesToLoad.Add("mail");
                        search.PropertiesToLoad.Add("physicalDeliveryOfficeName");
                        search.PropertiesToLoad.Add("company");
                        search.PropertiesToLoad.Add("st");

                        System.DirectoryServices.SearchResult result = search.FindOne();

                        if (result == null) return new User();

                        string displayName = "";
                        string givenName = "";
                        string sn = "";
                        string mail = "";
                        string physicalDeliveryOfficeName = "";
                        string company = "";
                        string st = "";

                        if (result.Properties.Contains("displayName")) displayName = (string)result.Properties["displayName"][0];
                        if (result.Properties.Contains("givenName")) givenName = (string)result.Properties["givenName"][0];
                        if (result.Properties.Contains("sn")) sn = (string)result.Properties["sn"][0];
                        if (result.Properties.Contains("mail")) mail = (string)result.Properties["mail"][0];
                        if (result.Properties.Contains("physicalDeliveryOfficeName")) physicalDeliveryOfficeName = (string)result.Properties["physicalDeliveryOfficeName"][0];
                        if (result.Properties.Contains("company")) company = (string)result.Properties["company"][0];
                        if (result.Properties.Contains("st")) st = (string)result.Properties["st"][0];

                        return new User(user, displayName, givenName, sn, mail, physicalDeliveryOfficeName, company, st);

                    }
                }
            }
            return new User();
        }

        private static string ComputeHash(string val)
        {
            byte[] tmpSource = ASCIIEncoding.ASCII.GetBytes(val);
            byte[] tmpHash = new MD5CryptoServiceProvider().ComputeHash(tmpSource);
            return System.Convert.ToBase64String(tmpHash);
        }

        private static string GetRandomCode(Random rdm)
        {
            string rndStr = "";
            for (int i = 0; i < 20; i++)
            {
                string rndTmp = rdm.Next(9).ToString();
                rndStr = rndStr + rndTmp;
            }
            return rndStr;
        }

        private static void Cleaning()
        {
            HttpApplicationState app = HttpContext.Current.Application;
            object dt = app["oauth_cleaning"];
            if (dt == null || (DateTime.Now - (DateTime)dt).TotalSeconds > 60)
            {
                app["oauth_cleaning"] = DateTime.Now;
                //cleaning

                foreach (string key in app.AllKeys)
                {
                    if (key.StartsWith(CODE_PREPEND))
                    {
                        if ((DateTime.Now - ((User)app[key]).timestamp).TotalSeconds > CODE_EXPIRATION)
                        {
                            app.Remove(key);
                        }
                    }
                    else if (key.StartsWith(TOKEN_PREPEND))
                    {
                        if ((DateTime.Now - ((User)app[key]).timestamp).TotalSeconds > TOKEN_EXPIRATION)
                        {
                            app.Remove(key);
                        }
                    }
                    else if (key.StartsWith(REFRESH_PREPEND))
                    {
                        if (app[TOKEN_PREPEND + ((Token)app[key]).access_token] == null)
                        {
                            app.Remove(key);
                        }
                    }
                }
            }
        }
    }
}